﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GameLib.Serialization;
using System.Xml;
using System.Reflection;
using System.IO;
using GameLib.Utils;

namespace Sandbox.Serialization
{
	/// <summary>
	/// Base serializer class handling xml content
	/// </summary>
	public abstract class XmlSerializer : Serializer
	{
		public override DataType DataType
		{
			get { return GameDataTypes.Xml; }
		}

		public override bool Deserialize(object obj, string dataPath)
		{
			if (obj == null)
				throw new ArgumentNullException();

			if (!File.Exists(dataPath))
				throw new Exception(string.Format("Could not find file based on data path: {0}, hence cannot deserialize its xml content", dataPath));

			XmlReaderSettings rSettings = new XmlReaderSettings();
			rSettings.CloseInput = true;
			rSettings.CheckCharacters = false;
			using (XmlReader reader = XmlReader.Create(dataPath, rSettings))
			{
				reader.ReadOuterXml();
				reader.MoveToContent();
				Deserialize(obj, obj.GetType(), reader, DataFlags.None);
			}

			return true;
		}

		public override bool Serialize(object obj, string dataPath)
		{
			if (obj == null)
				throw new ArgumentNullException();

			if (File.Exists(dataPath))
				File.Delete(dataPath);

			DirectoryInfo di = new DirectoryInfo(Path.GetDirectoryName(dataPath));
			if (!di.Exists)
				di.Create();

			XmlDocument document = new XmlDocument();
			XmlWriterSettings wSettings = new XmlWriterSettings();
			wSettings.Indent = true;
			wSettings.Encoding = Encoding.UTF8;
			wSettings.CloseOutput = true;
			wSettings.CheckCharacters = false;
			using (XmlWriter writer = XmlWriter.Create(dataPath, wSettings))
			{
				writer.WriteStartDocument();
				Serialize(obj, obj.GetType(), null, writer, DataFlags.None);
				writer.WriteEndDocument();
				writer.Flush();
				document.Save(writer);
			}

			return true;
		}

		public virtual object Deserialize(object obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			// reader should be positioned at start tag
			if (reader.NodeType != XmlNodeType.Element)
				throw new Exception(string.Format("XmlReader must be placed at the start of an element before calling XmlSerializer.Deserializer, current node:{0}, type: {1}", reader.Name, reader.NodeType));

			bool hasAttributes = reader.HasAttributes;
			if (hasAttributes)
			{
				// create a new object if obj is null but attributes are presented (this usually indicates we are creating a new object)
				if (obj == null)
					obj = CreateNewObject(type);

				DeserializeAttributes(obj, reader, dataFlags);
				reader.MoveToElement();
			}

			// if there is inner content
			if (!reader.IsEmptyElement)
			{
				// read to the node after the start tag
				reader.Read();
				reader.MoveToContent();

				// in case we already meet the end element (EX. <Book></Book>)
				// TODO: is this case covered by IsEmptyElement
				if (reader.NodeType != XmlNodeType.EndElement)
				{
					// obj = DeserializeInner(obj, type, reader, dataFlags);
					if (IsStringElement(obj, type, dataFlags))
					{
						obj = DeserializeInnerText(obj, type, reader, dataFlags);
					}
					else
					{
						// still no object, have to create one now
						if (obj == null)
							obj = CreateNewObject(type);

						obj = DeserializeInner(obj, type, reader, dataFlags);
					}
				}

				// read out the end node
				reader.ReadEndElement();
			}
			else
			{
				// read out the current element
				reader.Read();
			}

			// NOTE: if we never get into DeserializeAttributes or DeserializeInner, obj may still be null here
			// but it is probably fine, since an empty element with only a property name there probably doesn't make any sense
			// although we may just want to instantiate it with all default values??
			// TODO: verify above...

			// could probably just do this...
			if (obj == null && (dataFlags & DataFlags.Reference) != DataFlags.Reference)
				obj = CreateNewObject(type);

			return obj;
		}

		protected virtual void DeserializeAttributes(object obj, XmlReader reader, DataFlags dataFlags)
		{
			Type type = obj.GetType();
			while (reader.MoveToNextAttribute())
			{
				PropertyInfo pi = type.GetProperty(reader.Name);
				if (pi != null && pi.CanWrite)
				{
					pi.SetValue(obj, ConvertFromString(null, pi.PropertyType, reader.Value, dataFlags), null);
				}
				else
				{
					FieldInfo fi = type.GetField(reader.Name);
					if (fi != null)
					{
						fi.SetValue(obj, ConvertFromString(null, fi.FieldType, reader.Value, dataFlags));
					}
					else if (!IsKeyWord(reader.Name))
					{
						throw new Exception(string.Format("Unknown field/property {0} in {1} when deserializing attributes", reader.Name, obj));
					}
				}

				//Console.WriteLine(" {0}={1}", reader.Name, reader.Value);
			}
		}

		protected bool IsKeyWord(string str)
		{
			return str == KeyWord_Key || str == KeyWord_Type;
		}

		/*
		protected virtual object DeserializeInner(object obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			// TODO: should check IsStringElement
			if (reader.NodeType == XmlNodeType.Text)
			{
				return DeserializeInnerText(obj, type, reader, dataFlags);
			}
			else if (reader.NodeType == XmlNodeType.Element)
			{
				// must have an object now
				if (obj == null)
					obj = CreateNewObject(type);

				return DeserializeInnerElements(obj, type, reader, dataFlags);
			}
			else
			{
				throw new Exception(string.Format("Not supported inner node type: {0}", reader.NodeType));
			}
		}*/

		protected virtual object DeserializeInnerText(object obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			if (reader.NodeType != XmlNodeType.Text)
				throw new Exception(string.Format("Encounter non-text node type when trying to deserialize inner text {0} {1} : {2}", reader.NodeType, reader.Name, reader.Value));

			object retObj = ConvertFromString(obj, type, reader.Value, dataFlags);

			// read out the text node
			reader.Read();
			reader.MoveToContent();

			// make sure we reach the end element
			if (reader.NodeType != XmlNodeType.EndElement)
				throw new Exception();

			return retObj;			
		}

		protected object ConvertFromString(object obj, Type type, string str, DataFlags dataFlag)
		{
			StringSerializer strSerializer = DataHandler.GetSerializer(type, GameDataTypes.Text) as StringSerializer;
			if (strSerializer == null)
				throw new Exception(string.Format("Cannot find string serializer for type {0}", type));

			// TODO: check if the serializer does not accept given obj, is so set obj to null

			return strSerializer.ConvertFromString(obj, type, str, dataFlag);
		}

		protected virtual object DeserializeInner(object obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			return DeserializeProperties(obj, type, reader, dataFlags);
		}

		protected virtual object DeserializeProperties(object obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			do
			{
				if (reader.NodeType != XmlNodeType.Element)
				{
					throw new Exception();
				}

				string propName = reader.Name;

				PropertyInfo pi = type.GetProperty(propName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

				if (pi == null)
				{
					throw new Exception();
				}

				Type propType = pi.PropertyType;

				// see if a sub type is indicated

				Type instType = propType;

				string instTypeName = reader.GetAttribute(KeyWord_Type);

				if (instTypeName != null)
				{
					instType = TypeNameTable.GetType(instTypeName);

					if (!instType.IsSubclassOf(propType))
						throw new Exception(string.Format("Type {0} is not a sub class of Type {1} cannot be used as sub object type!", instType, propType));
				}

				// data flags for this property
				DataFlags propDataFlags = GetPropertyDataFlags(pi);

				// see if we need to pass over the existing property value
				object curValue = null;

				// not value type, not reference, instance type same as property type
				if (!propType.IsValueType &&
					(propDataFlags & DataFlags.Reference) != DataFlags.Reference &&
					instType == propType)
				{
					curValue = pi.GetValue(obj, null);
				}

				object newValue;
				
				if (!DeserializePropertyByHandler(curValue, instType, pi.Name, reader, dataFlags, out newValue))
					newValue = DeserializeSubObject(curValue, instType, reader, propDataFlags);

				// set new value if necessary
				if (curValue == null || newValue != curValue)
				{
					pi.SetValue(obj, curValue, null);
				}

				// now it should be at the node after the object end tag
				reader.MoveToContent();

			} while (reader.NodeType != XmlNodeType.EndElement && reader.NodeType != XmlNodeType.None);

			return obj;
		}

		protected bool DeserializePropertyByHandler(object obj, Type type, string propName, XmlReader reader, DataFlags dataFlags, out object newObj)
		{
			XmlPropertyHandler handler;

			if (PropertyHandlers.TryGetValue(propName, out handler))
			{
				newObj = handler.Deserializer(obj, type, propName, reader, dataFlags);
				return true;
			}
			else
			{
				newObj = null;
				return false;
			}
		}

		protected object DeserializeSubObject(object obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			// find out beloved serializer
			XmlSerializer xmlSerializer = DataHandler.GetSerializer(type, GameDataTypes.Xml) as XmlSerializer;

			if (xmlSerializer == null)
				throw new Exception();

			// deserialize the property value
			return xmlSerializer.Deserialize(obj, type, reader, dataFlags);
		}

		protected DataFlags GetPropertyDataFlags(PropertyInfo pi)
		{
			object[] attrs = pi.GetCustomAttributes(typeof(DataPropertyAttribute), false);
			return attrs.Length > 0 ? (attrs[0] as DataPropertyAttribute).DataFlags : DataFlags.None;
		}

		protected object CreateNewObject(Type type)
		{
			return type.Assembly.CreateInstance(type.ToString());
		}

		public virtual void Serialize(object obj, Type type, string elemName, XmlWriter writer, DataFlags dataFlags)
		{
			// TODO: grab type alternative name from type's attr
			writer.WriteStartElement(elemName != null ? elemName : type.Name);

			if (IsStringElement(obj, type, dataFlags) || obj == null)
			{
				SerializeInnerText(obj, type, writer, dataFlags);
			}
			else
			{
				if (IsStruct(type))
				{
					SerializeStruct(obj, writer, dataFlags);
				}
				else
				{
					List<PropertyInfo> attrProps = new List<PropertyInfo>(10);
					List<PropertyInfo> elemProps = new List<PropertyInfo>(10);
					foreach (PropertyInfo pi in type.GetProperties(BindingFlags.Instance|BindingFlags.Public|BindingFlags.NonPublic))
					{
						XmlDataPropertyAttribute attr = Attribute.GetCustomAttribute(pi, typeof(XmlDataPropertyAttribute), true) as XmlDataPropertyAttribute;

						if (attr != null)
						{
							if (attr.IsAttribute)
								attrProps.Add(pi);
							else
								elemProps.Add(pi);
						}
					}

					// write attributes
					SerializeAttributes(obj, attrProps, writer, dataFlags);

					// write inner
					SerializeInner(obj, elemProps, writer, dataFlags);
				}
			}

			writer.WriteEndElement();
		}

		protected bool IsStringElement(object obj, Type type, DataFlags dataFlags)
		{
			return type.IsPrimitive || type.IsEnum || type == typeof(string) || (dataFlags & DataFlags.Reference) == DataFlags.Reference;
		}

		protected bool IsStruct(Type type)
		{
			return type.IsValueType && !type.IsPrimitive && !type.IsEnum;		
		}
		
		protected virtual void SerializeAttributes(object obj, List<PropertyInfo> props, XmlWriter writer, DataFlags dataFlags)
		{
			foreach (PropertyInfo pi in props)
			{
				object attrValue = pi.GetValue(obj, null);
				Type attrType = attrValue != null ? attrValue.GetType() : pi.PropertyType;

				// TODO: grab data flags from pi's attribute
				// TODO: grab alternative prop name?
				writer.WriteAttributeString(pi.Name, ConvertToString(attrValue, attrType, DataFlags.None));
			}
		}

		protected virtual void SerializeInner(object obj, List<PropertyInfo> props, XmlWriter writer, DataFlags dataFlags)
		{
			SerializeProperties(obj, props, writer, dataFlags);
		}

		protected virtual void SerializeProperties(object obj, List<PropertyInfo> props, XmlWriter writer, DataFlags dataFlags)
		{
			foreach (PropertyInfo pi in props)
			{
				object elemValue = pi.GetValue(obj, null);
				Type elemType = elemValue != null ? elemValue.GetType() : pi.PropertyType;

				if (!SerializePropertyByHandler(elemValue, elemType, pi.Name, writer, dataFlags))
				{
					// TODO: grab data flags from pi's attribute
					// TODO: grab alternative prop name?
					SerializeSubObject(elemValue, elemType, pi.Name, writer, DataFlags.None);
				}
			}
		}

		protected bool SerializePropertyByHandler(object obj, Type type, string propName, XmlWriter writer, DataFlags dataFlags)
		{
			XmlPropertyHandler handler;

			if (PropertyHandlers.TryGetValue(propName, out handler))
			{
				//writer.WriteStartElement(propName);
				handler.Serializer(obj, type, propName, writer, dataFlags);
				//writer.WriteEndElement();
				return true;
			}
			return false;
		}

		protected virtual void SerializeStruct(object obj, XmlWriter writer, DataFlags dataFlags)
		{
			Type type = obj.GetType();
			List<FieldInfo> attrFields = new List<FieldInfo>(10);
			List<FieldInfo> elemFields = new List<FieldInfo>(10);
			foreach (FieldInfo fi in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
			{
				XmlDataPropertyAttribute attr = Attribute.GetCustomAttribute(fi, typeof(XmlDataPropertyAttribute), true) as XmlDataPropertyAttribute;

				if (attr == null || attr.IsAttribute)
					attrFields.Add(fi);
				else
					elemFields.Add(fi);
			}

			foreach (FieldInfo fi in attrFields)
			{
				object attrValue = fi.GetValue(obj);
				Type attrType = attrValue != null ? attrValue.GetType() : fi.FieldType;

				// TODO: grab data flags from fi's attribute
				// TODO: grab alternative field name?
				writer.WriteAttributeString(fi.Name, ConvertToString(attrValue, attrType, DataFlags.None));
			}

			foreach (FieldInfo fi in elemFields)
			{
				object elemValue = fi.GetValue(obj);
				Type elemType = elemValue != null ? elemValue.GetType() : fi.FieldType;

				// TODO: grab data flags from fi's attribute
				// TODO: grab alternative field name?
				SerializeSubObject(elemValue, elemType, fi.Name, writer, DataFlags.None);
			}
		}

		protected virtual void SerializeInnerText(object obj, Type type, XmlWriter writer, DataFlags dataFlags)
		{
			writer.WriteString(ConvertToString(obj, type, dataFlags));
		}

		protected void SerializeSubObject(object obj, Type type, string elemName, XmlWriter writer, DataFlags dataFlags)
		{
			// find out beloved serializer
			XmlSerializer xmlSerializer = DataHandler.GetSerializer(type, GameDataTypes.Xml) as XmlSerializer;

			if (xmlSerializer == null)
				throw new Exception();

			// deserialize the property value
			xmlSerializer.Serialize(obj, type, elemName, writer, dataFlags);
		}
		
		protected string ConvertToString( object obj, Type type,DataFlags dataFlag)
		{
			StringSerializer strSerializer = DataHandler.GetSerializer(type, GameDataTypes.Text) as StringSerializer;
			if (strSerializer == null)
				throw new Exception(string.Format("Cannot find string serializer for type {0}", type));

			// TODO: check if the serializer does not accept given obj, is so set obj to null

			return strSerializer.ConvertToString(obj, dataFlag);
		}

		protected void RegisterPropertyHandler(string propName, XmlPropertyDeserializer deserializer, XmlPropertySerializer serializer)
		{
			XmlPropertyHandler handler = new XmlPropertyHandler();
			handler.Deserializer = deserializer;
			handler.Serializer = serializer;
			PropertyHandlers[propName] = handler;
		}

		public const string KeyWord_Key = "Key";
		public const string KeyWord_Type = "Type";

		private Dictionary<string, XmlPropertyHandler> PropertyHandlers = new Dictionary<string, XmlPropertyHandler>();
	}

	public delegate object XmlPropertyDeserializer(object obj, Type type, string propName, XmlReader reader, DataFlags dataFlags);
	public delegate void XmlPropertySerializer(object obj, Type type, string propName, XmlWriter writer, DataFlags dataFlags);

	public struct XmlPropertyHandler
	{
		public XmlPropertyDeserializer Deserializer;
		public XmlPropertySerializer Serializer;
	}

	public abstract class XmlSerializer<T> : XmlSerializer
	{
		protected override object DeserializeInner(object obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			return DeserializeInnerTyped((T)obj, type, reader, dataFlags);
		}

		protected virtual object DeserializeInnerTyped(T obj, Type type, XmlReader reader, DataFlags dataFlags)
		{
			return DeserializeProperties(obj, type, reader, dataFlags);
		}
		
		protected override void SerializeInner(object obj, List<PropertyInfo> props, XmlWriter writer, DataFlags dataFlags)
		{
			SerializeInnerTyped((T)obj, props, writer, dataFlags);
		}

		protected virtual void SerializeInnerTyped(T obj, List<PropertyInfo> props, XmlWriter writer, DataFlags dataFlags)
		{
			SerializeProperties(obj, props, writer, dataFlags);
		}

		public override Type SupportedType
		{
			get { return typeof(T); }
		}
	}

}
